Branching

  • Create the feature/authentication branch based on develop

Adding Models

Create three files inside this folder:
📂 Suggested Folder: shared/models/authentication

  • AuthResponseDto.ts
export interface AuthResponseDto {
  token: string;
  phoneNumber: string;
  roles: string[];
}
  • LoginRequestDto.ts
export interface LoginRequestDto {
  phoneNumber: string;
  password: string;
}
  • RegisterRequestDto.ts
export interface RegisterRequestDto {
  phoneNumber: string;
  password: string;
  confirmPassword: string;
}

Adding Authentication API Calls in agent.ts

  • Add the Auth object to agent.ts:
const Auth = {
register: (data: RegisterRequestDto) =>
    request.post<{ token: string }>('/auth/register', data),
login: (data: LoginRequestDto) =>
    request.post<{ token: string }>('/auth/login', data),
};
  • Ensure agent.ts ends like this:
const agent = {
    TransportationSearch,
    Cities,
    Auth
}

Creating authStore.ts

Suggested Folder
📂 Suggested Folder: shared/store/

  • Create authStore.ts
import { AuthResponseDto } from '@/shared/models/authentication/AuthResponseDto';
import {create} from 'zustand';

interface User {
  phoneNumber: string;
  roles: string[];
}

  

interface AuthState {
  isLoggedIn: boolean;
  user: User | null;
  token: string | null;
  login: (response: AuthResponseDto) => void;
  logout: () => void;
  setToken: (token: string) => void;
}

  

export const useAuthStore = create<AuthState>((set) => ({
  isLoggedIn: false,
  user: null,
  token: null,


  login: (response) =>
  set(() => ({
    token: response.token,
    user: {
      phoneNumber: response.phoneNumber,
      roles: response.roles
    },

    isLoggedIn: true,
  })),

  logout: () =>
    set(() => ({
      token: null,
      user: null,
      isLoggedIn: false,
    })),

  

  setToken: (token) =>
    set((state) => ({
      token,
      isLoggedIn: !!token,
      user: state.user,
    })),
}));

About authStore.ts

This file defines a centralized authentication state store using Zustand, a minimal and scalable state management library for React. It helps manage login state, user data, and authentication token across your application.


🔹 User Interface

interface User {
  phoneNumber: string;
  roles: string[];
}

This interface defines the shape of the user object stored in the auth state. It currently includes:

  • phoneNumber: A string representing the user's phone number.
  • roles: An array representing user roles

🔹 AuthState Interface

interface AuthState {
  isLoggedIn: boolean;
  user: User | null;
  token: string | null;
  login: (response: AuthResponseDto) => void;
  logout: () => void;
  setToken: (token: string) => void;
}

This defines the overall structure of the authentication store:

  • isLoggedIn: Indicates whether a user is logged in.
  • user: Stores user-specific data if authenticated; otherwise null.
  • token: JWT or access token from the server.
  • login(): Accepts an AuthResponseDto and updates the state.
  • logout(): Clears all authentication-related data.
  • setToken(): Sets the token and toggles login status accordingly.

🔹 Zustand Store Definition

export const useAuthStore = create<AuthState>((set) => ({ ... });

Creates a global auth store using Zustand. create() accepts a function that receives set (used to update state) and returns the initial store state and methods.


🔹 Initial State

isLoggedIn: false,
user: null,
token: null,

These lines define the initial, default state for an unauthenticated user.


🔹 login() Method

login: (response) =>
  set(() => ({
    token: response.token,
    user: {
      phoneNumber: response.phoneNumber,
      roles: response.roles
    },
    isLoggedIn: true,
  })),
  • Accepts an AuthResponseDto object after a successful login.
  • Extracts the token, phoneNumber, and roles, and sets them in state.
  • Marks the user as isLoggedIn: true.

🔹 logout() Method

logout: () =>
  set(() => ({
    token: null,
    user: null,
    isLoggedIn: false,
  })),
  • Clears all auth-related data (token and user).
  • Effectively logs the user out by setting isLoggedIn to false.

🔹 setToken() Method

setToken: (token) =>
  set((state) => ({
    token,
    isLoggedIn: !!token,
    user: state.user,
  })),
  • Updates the token in the store.
  • Sets isLoggedIn based on whether a non-empty token exists.
  • Retains the current user object.

Add LoginModal

📂 Suggested Folder: shared/features/authentication/modals

Example

import React, { useState } from "react";
import { useAuthStore } from "@/store/authStore";
import agent from "@/shared/api/agent";
import { LoginRequestDto } from "@/shared/models/authentication/LoginRequestDto";

interface LoginModalProps {
  onClose: () => void;
}

  

const LoginModal: React.FC<LoginModalProps> = ({ onClose }) => {
  
  const login = useAuthStore((state) => state.login);

  const [form, setForm] = useState<LoginRequestDto>({
    phoneNumber: "",
    password: "",
  });

  const [error, setError] = useState<string | null>(null);

  const validate = () => {
    const phoneRegex = /^(?:\+98|0)?9\d{9}$/;
    if (!phoneRegex.test(form.phoneNumber)) {
      return "Invalid phone number format";
    }

    if (!form.password || form.password.length < 8) {
      return "Password must be at least 8 characters";
    }
    return null;
  };

  const handleSubmit = async () => {
    const validationError = validate();
    if (validationError) {
      setError(validationError);
      return;
    }

    try {
      const response = await agent.Auth.login(form);
      login(response);
      setError(null);
      onClose();
    } catch (err: any) {
      setError(err.response?.data?.message || "Login failed");
    }
  };

  

  return (
    <div style={styles.overlay}>
      <div style={styles.modal}>
        <h2 style={{ marginBottom: "1rem" }}>Login</h2>
        <input
          type="text"
          placeholder="Phone Number"
          value={form.phoneNumber}
          onChange={(e) => setForm({ ...form, phoneNumber: e.target.value })}
          style={styles.input}
        />

        <input
          type="password"
          placeholder="Password"
          value={form.password}
          onChange={(e) => setForm({ ...form, password: e.target.value })}
          style={styles.input}
        />
        <button onClick={handleSubmit} style={styles.button}>
          Login
        </button>
        {error && (
          <p style={{ color: "red", marginTop: "0.5rem", fontWeight: "bold" }}>
            {error}
          </p>
        )}

        <button
          onClick={onClose}
          style={{
            ...styles.button,
            marginTop: "0.5rem",
            backgroundColor: "#ccc",
            color: "#333",
          }}
        >
          Cancel
        </button>
      </div>
    </div>
  );
};

const styles: { [key: string]: React.CSSProperties } = {
	//ADD STYLES
};

export default LoginModal;

About LoginModal

This component provides a modal UI that allows users to log in using their phone number and password. It integrates with the authentication store and API to perform login logic and handle errors.


🔹 Props Interface

interface LoginModalProps {
  onClose: () => void;
}
  • onClose: A callback to be called when the modal should be closed (e.g., user clicks "Cancel" or logs in successfully).

🔹 Component Setup

const LoginModal: React.FC<LoginModalProps> = ({ onClose }) => { ... };

Defines a functional React component with the onClose prop destructured.

🔸 Accessing Auth Store

const login = useAuthStore((state) => state.login);

Retrieves the login method from Zustand’s authStore so that the global auth state can be updated after successful login.

🔸 Local Form State

const [form, setForm] = useState<LoginRequestDto>({
  phoneNumber: "",
  password: "",
});

Initializes form state with empty values for the phone number and password.

🔸 Error Handling State

const [error, setError] = useState<string | null>(null);

Stores any error messages resulting from validation or login attempt.


🔹 Validation Logic

const validate = () => {
  const phoneRegex = /^(?:\+98|0)?9\d{9}$/;
  if (!phoneRegex.test(form.phoneNumber)) {
    return "Invalid phone number format";
  }

  if (!form.password || form.password.length < 8) {
    return "Password must be at least 8 characters";
  }
  return null;
};
  • Validates the phone number format (Iranian phone format in this case).
  • Ensures password is at least 8 characters long.
  • Returns a string error message or null if validation passes.

🔹 Submit Handler

const handleSubmit = async () => {
  const validationError = validate();
  if (validationError) {
    setError(validationError);
    return;
  }

  try {
    const response = await agent.Auth.login(form);
    login(response);
    setError(null);
    onClose();
  } catch (err: any) {
    setError(err.response?.data?.message || "Login failed");
  }
};
  • Calls validate() and prevents submission if there's an error.
  • Calls the backend API using agent.Auth.login().
  • On success: updates auth state, clears error, closes modal.
  • On failure: shows error message.

🔹 UI Layout

return (
  <div style={styles.overlay}>
    <div style={styles.modal}>
      <h2>Login</h2>
      <input ... />
      <input ... />
      <button onClick={handleSubmit}>Login</button>
      {error && <p>{error}</p>}
      <button onClick={onClose}>Cancel</button>
    </div>
  </div>
);

Elements:

  • Phone Number Input
  • Password Input
  • Login Button: Triggers handleSubmit.
  • Error Message: Shown only if there's an error.
  • Cancel Button: Triggers onClose callback.

🔹 Styles Placeholder

const styles: { [key: string]: React.CSSProperties } = {
  // Add modal styles here
};

This placeholder defines inline CSS styles for the modal. Each style (e.g., overlay, modal, input, button) should be defined here.


✅ Summary

This modal:

  • Provides a simple, reusable login form.
  • Validates input before calling the API.
  • Updates global auth state via Zustand.
  • Handles success/failure states.
  • Uses modal-friendly inline styles (with room for improvement).

Add RegisterModal

  • Create RegisterModal
    📂 Suggested Folder: shared/features/authentication/modals
import agent from "@/shared/api/agent";
import { RegisterRequestDto } from "@/shared/models/authentication/RegisterRequestDto";
import { useAuthStore } from "@/store/authStore";
import React, { useState } from "react";


interface Props {
  onClose: () => void;
}


const RegisterModal: React.FC<Props> = ({ onClose }) => {

  const [form, setForm] = useState<RegisterRequestDto>({
    phoneNumber: "",
    password: "",
    confirmPassword: "",
  });

  const [error, setError] = useState<string | null>(null);

  const login = useAuthStore((state) => state.login);
  
  const validate = () => {

    const { phoneNumber, password, confirmPassword } = form;
    if (!phoneNumber || !password || !confirmPassword) {
      return "All fields are required.";
    }

    if (!/^\d{11}$/.test(phoneNumber)) {
      return "Phone number must be 11 digits.";
    }

    if (password.length < 6) {
      return "Password must be at least 6 characters.";
    }

    if (password !== confirmPassword) {
      return "Passwords do not match.";
    }

    return null;
  };

  

  const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    setForm({ ...form, [e.target.name]: e.target.value });
  };

  

  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault();
    setError(null);
    
    const validationError = validate();

    if (validationError) {
      setError(validationError);
      return;
    }

  

    try {

      // Use the form as RegisterRequestDto explicitly

      const requestData: RegisterRequestDto = {
        phoneNumber: form.phoneNumber,
        password: form.password,
        confirmPassword: form.confirmPassword,
      };

  

      const response = await agent.Auth.register(requestData);
      login(response);
      setForm({ phoneNumber: "", password: "", confirmPassword: "" });
      onClose();
    } catch (err: any) {
      setError(err.response?.data?.message || "Registration failed.");
    }
  };

  return (
    <div style={styles.overlay}>
      <div style={styles.modal}>
        <h2 style={{ marginBottom: "1rem" }}>Register</h2>
        <form
          onSubmit={handleSubmit}
          style={{ display: "flex", flexDirection: "column" }}
        >

          <input
            type="text"
            name="phoneNumber"
            value={form.phoneNumber}
            onChange={handleChange}
            placeholder="Phone Number"
            style={styles.input}
          />

          <input
            type="password"
            name="password"
            value={form.password}
            onChange={handleChange}
            placeholder="Password"
            style={styles.input}
          />

          <input
            type="password"
            name="confirmPassword"
            value={form.confirmPassword}
            onChange={handleChange}
            placeholder="Confirm Password"
            style={styles.input}
          />

          {error && (

            <p
              style={{ color: "red", marginTop: "0.5rem", fontWeight: "bold" }}
            >
              {error}
            </p>
          )}

          <button type="submit" style={styles.button}>
            Register
          </button>

        </form>

        <button

          onClick={onClose}

          style={{
            ...styles.button,
            marginTop: "0.5rem",
            backgroundColor: "#ccc",
            color: "#333",
          }}
        >
          Cancel
        </button>
      </div>
    </div>
  );
};

  

const styles: { [key: string]: React.CSSProperties } = {
	//ADD STYLES
};

export default RegisterModal;

About RegisterModal

Component Structure

1. Props

interface Props {
  onClose: () => void;
}
  • The modal only expects one prop: onClose, a function to close the modal (e.g., hide it from the screen).

2. State Management

const [form, setForm] = useState<RegisterRequestDto>({
  phoneNumber: "",
  password: "",
  confirmPassword: "",
});
  • Initializes the form state for inputs, based on the RegisterRequestDto shape.
const [error, setError] = useState<string | null>(null);
  • Stores any validation or server error message to display in the UI.
const login = useAuthStore((state) => state.login);
  • Accesses the login method from your global auth store, to automatically log in the user after successful registration.

3. Validation Logic

const validate = () => {
  // Checks for empty fields
  // Validates phone number format (must be 11 digits)
  // Ensures password length is sufficient
  // Confirms password and confirmation match
};
  • Ensures client-side validation before making a request to the server.

4. Input Handling

const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
  setForm({ ...form, [e.target.name]: e.target.value });
};
  • Updates the correct field in the form object dynamically based on the input name.

5. Form Submission

const handleSubmit = async (e: React.FormEvent) => {
  e.preventDefault();
  setError(null);
  const validationError = validate();
  // If validation passes, submit the data to the backend
  // If backend response is successful, log in and close modal
  // If it fails, show error message
};
  • Prevents default form submission
  • Validates inputs
  • Sends the data to agent.Auth.register
  • On success: logs in user and clears form
  • On failure: shows error from server

6. JSX Render

<div style={styles.overlay}>...</div>
  • Modal Overlay: darkened background behind the modal
  • Modal Box: contains title, form, and buttons

Inside <form>:

  • Inputs for:
    - phoneNumber
    - password
    - confirmPassword
  • Submit button for Register
  • Error message display (if any)
  • Cancel button that calls onClose

Handle login/logout and register buttons in navbar

  • implement a way for showing login and register buttons in navbar when user is not signed in
  • when clicked, the button should show the related modal, for the user to sign in or register

Merge

  • Create a PR and merge the current branch with develop